/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.

This file is part of XreaL source code.

XreaL source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

XreaL source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with XreaL source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/
// sv_rankings.c -- global rankings interface

#include "server.h"
#include "..\rankings\1.0\gr\grapi.h"
#include "..\rankings\1.0\gr\grlog.h"

typedef struct
{
	GR_CONTEXT      context;
	uint64_t        game_id;
	uint64_t        match;
	uint64_t        player_id;
	GR_PLAYER_TOKEN token;
	grank_status_t  grank_status;
	grank_status_t  final_status;	// status to set after cleanup
	uint32_t        grank;		// global rank
	char            name[32];
} ranked_player_t;

static int      s_rankings_contexts = 0;
static qboolean s_rankings_active = qfalse;
static GR_CONTEXT s_server_context = 0;
static uint64_t s_server_match = 0;
static char    *s_rankings_game_key = NULL;
static uint64_t s_rankings_game_id = 0;
static ranked_player_t *s_ranked_players = NULL;
static qboolean s_server_quitting = qfalse;
static const char s_ascii_encoding[] = "0123456789abcdef" "ghijklmnopqrstuv" "wxyzABCDEFGHIJKL" "MNOPQRSTUVWXYZ[]";

// private functions
static void     SV_RankNewGameCBF(GR_NEWGAME * gr_newgame, void *cbf_arg);
static void     SV_RankUserCBF(GR_LOGIN * gr_login, void *cbf_arg);
static void     SV_RankJoinGameCBF(GR_JOINGAME * gr_joingame, void *cbf_arg);
static void     SV_RankSendReportsCBF(GR_STATUS * gr_status, void *cbf_arg);
static void     SV_RankCleanupCBF(GR_STATUS * gr_status, void *cbf_arg);
static void     SV_RankCloseContext(ranked_player_t * ranked_player);
static int      SV_RankAsciiEncode(char *dest, const unsigned char *src, int src_len);
static int      SV_RankAsciiDecode(unsigned char *dest, const char *src, int src_len);
static void     SV_RankEncodeGameID(uint64_t game_id, char *result, int len);
static uint64_t SV_RankDecodePlayerID(const char *string);
static void     SV_RankDecodePlayerKey(const char *string, GR_PLAYER_TOKEN key);
static char    *SV_RankStatusString(GR_STATUS status);
static void     SV_RankError(const char *fmt, ...);
static char     SV_RankGameKey[64];

/*
================
SV_RankBegin
================
*/
void SV_RankBegin(char *gamekey)
{
	GR_INIT         init;
	GR_STATUS       status;

	assert(s_rankings_contexts == 0);
	assert(!s_rankings_active);
	assert(s_ranked_players == NULL);

	if(sv_enableRankings->integer == 0 || Cvar_VariableValue("g_gametype") == GT_SINGLE_PLAYER)
	{
		s_rankings_active = qfalse;
		if(sv_rankingsActive->integer == 1)
		{
			Cvar_Set("sv_rankingsActive", "0");
		}
		return;
	}

	// only allow official game key on pure servers
	if(strcmp(gamekey, GR_GAMEKEY) == 0)
	{
/*
		if( Cvar_VariableValue("sv_pure") != 1 )
		{
			Cvar_Set( "sv_enableRankings", "0" );
			return;
		}
*/

		// substitute game-specific game key
		switch ((int)Cvar_VariableValue("g_gametype"))
		{
			case GT_FFA:
				gamekey = "Q3 Free For All";
				break;
			case GT_TOURNAMENT:
				gamekey = "Q3 Tournament";
				break;
			case GT_TEAM:
				gamekey = "Q3 Team Deathmatch";
				break;
			case GT_CTF:
				gamekey = "Q3 Capture the Flag";
				break;
			case GT_1FCTF:
				gamekey = "Q3 One Flag CTF";
				break;
			case GT_OBELISK:
				gamekey = "Q3 Overload";
				break;
			case GT_HARVESTER:
				gamekey = "Q3 Harvester";
				break;
			default:
				break;
		}
	}
	s_rankings_game_key = gamekey;

	// initialize rankings
	GRankLogLevel(GRLOG_OFF);
	memset(SV_RankGameKey, 0, sizeof(SV_RankGameKey));
	strncpy(SV_RankGameKey, gamekey, sizeof(SV_RankGameKey) - 1);
	init = GRankInit(1, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END);
	s_server_context = init.context;
	s_rankings_contexts++;
	Com_DPrintf("SV_RankBegin(); GR_GAMEKEY is %s\n", gamekey);
	Com_DPrintf("SV_RankBegin(); s_rankings_contexts=%d\n", s_rankings_contexts);
	Com_DPrintf("SV_RankBegin(); s_server_context=%d\n", init.context);

	// new game
	if(!strlen(Cvar_VariableString("sv_leagueName")))
	{
		status = GRankNewGameAsync
			(s_server_context,
			 SV_RankNewGameCBF, NULL, GR_OPT_LEAGUENAME, (void *)(Cvar_VariableString("sv_leagueName")), GR_OPT_END);
	}
	else
	{
		status = GRankNewGameAsync(s_server_context, SV_RankNewGameCBF, NULL, GR_OPT_END);
	}

	if(status != GR_STATUS_PENDING)
	{
		SV_RankError("SV_RankBegin: Expected GR_STATUS_PENDING, got %s", SV_RankStatusString(status));
		return;
	}

	// logging
	if(com_developer->value)
	{
		GRankLogLevel(GRLOG_TRACE);
	}

	// allocate rankings info for each player
	s_ranked_players = Z_Malloc(sv_maxclients->value * sizeof(ranked_player_t));
	memset((void *)s_ranked_players, 0, sv_maxclients->value * sizeof(ranked_player_t));
}

/*
================
SV_RankEnd
================
*/
void SV_RankEnd(void)
{
	GR_STATUS       status;
	int             i;

	Com_DPrintf("SV_RankEnd();\n");

	if(!s_rankings_active)
	{
		// cleanup after error during game
		if(s_ranked_players != NULL)
		{
			for(i = 0; i < sv_maxclients->value; i++)
			{
				if(s_ranked_players[i].context != 0)
				{
					SV_RankCloseContext(&(s_ranked_players[i]));
				}
			}
		}
		if(s_server_context != 0)
		{
			SV_RankCloseContext(NULL);
		}

		return;
	}

	for(i = 0; i < sv_maxclients->value; i++)
	{
		if(s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE)
		{
			SV_RankUserLogout(i);
			Com_DPrintf("SV_RankEnd: SV_RankUserLogout %d\n", i);
		}
	}

	assert(s_server_context != 0);

	// send match reports, proceed to SV_RankSendReportsCBF
	status = GRankSendReportsAsync(s_server_context, 0, SV_RankSendReportsCBF, NULL, GR_OPT_END);

	if(status != GR_STATUS_PENDING)
	{
		SV_RankError("SV_RankEnd: Expected GR_STATUS_PENDING, got %s", SV_RankStatusString(status));
	}

	s_rankings_active = qfalse;
	Cvar_Set("sv_rankingsActive", "0");
}

/*
================
SV_RankPoll
================
*/
void SV_RankPoll(void)
{
	GRankPoll();
}

/*
================
SV_RankCheckInit
================
*/
qboolean SV_RankCheckInit(void)
{
	return (s_rankings_contexts > 0);
}

/*
================
SV_RankActive
================
*/
qboolean SV_RankActive(void)
{
	return s_rankings_active;
}

/*
=================
SV_RankUserStatus
=================
*/
grank_status_t SV_RankUserStatus(int index)
{
	if(!s_rankings_active)
	{
		return GR_STATUS_ERROR;
	}

	assert(s_ranked_players != NULL);
	assert(index >= 0);
	assert(index < sv_maxclients->value);

	return s_ranked_players[index].grank_status;
}

/*
================
SV_RankUserGRank
================
*/
int SV_RankUserGrank(int index)
{
	if(!s_rankings_active)
	{
		return 0;
	}

	assert(s_ranked_players != NULL);
	assert(index >= 0);
	assert(index < sv_maxclients->value);

	return s_ranked_players[index].grank;
}

/*
================
SV_RankUserReset
================
*/
void SV_RankUserReset(int index)
{
	if(!s_rankings_active)
	{
		return;
	}

	assert(s_ranked_players != NULL);
	assert(index >= 0);
	assert(index < sv_maxclients->value);

	switch (s_ranked_players[index].grank_status)
	{
		case QGR_STATUS_SPECTATOR:
		case QGR_STATUS_NO_USER:
		case QGR_STATUS_BAD_PASSWORD:
		case QGR_STATUS_USER_EXISTS:
		case QGR_STATUS_NO_MEMBERSHIP:
		case QGR_STATUS_TIMEOUT:
		case QGR_STATUS_ERROR:
			s_ranked_players[index].grank_status = QGR_STATUS_NEW;
			break;
		default:
			break;
	}
}

/*
================
SV_RankUserSpectate
================
*/
void SV_RankUserSpectate(int index)
{
	if(!s_rankings_active)
	{
		return;
	}

	assert(s_ranked_players != NULL);
	assert(index >= 0);
	assert(index < sv_maxclients->value);

	// GRANK_FIXME - check current status?
	s_ranked_players[index].grank_status = QGR_STATUS_SPECTATOR;
}

/*
================
SV_RankUserCreate
================
*/
void SV_RankUserCreate(int index, char *username, char *password, char *email)
{
	GR_INIT         init;
	GR_STATUS       status;

	assert(index >= 0);
	assert(index < sv_maxclients->value);
	assert(username != NULL);
	assert(password != NULL);
	assert(email != NULL);
	assert(s_ranked_players);
	assert(s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE);

	Com_DPrintf("SV_RankUserCreate( %d, %s, \"****\", %s );\n", index, username, email);

	if(!s_rankings_active)
	{
		Com_DPrintf("SV_RankUserCreate: Not ready to create\n");
		s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
		return;
	}

	if(s_ranked_players[index].grank_status == QGR_STATUS_ACTIVE)
	{
		Com_DPrintf("SV_RankUserCreate: Got Create from active player\n");
		return;
	}

	// get a separate context for the new user
	init = GRankInit(0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END);
	s_ranked_players[index].context = init.context;
	s_rankings_contexts++;
	Com_DPrintf("SV_RankUserCreate(); s_rankings_contexts=%d\n", s_rankings_contexts);
	Com_DPrintf("SV_RankUserCreate(); s_ranked_players[%d].context=%d\n", index, init.context);

	// attempt to create a new account, proceed to SV_RankUserCBF
	status = GRankUserCreateAsync
		(s_ranked_players[index].context,
		 username, password, email, SV_RankUserCBF, (void *)&s_ranked_players[index], GR_OPT_END);

	if(status == GR_STATUS_PENDING)
	{
		s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
		s_ranked_players[index].final_status = QGR_STATUS_NEW;
	}
	else
	{
		SV_RankError("SV_RankUserCreate: Expected GR_STATUS_PENDING, got %s", SV_RankStatusString(status));
	}
}

/*
================
SV_RankUserLogin
================
*/
void SV_RankUserLogin(int index, char *username, char *password)
{
	GR_INIT         init;
	GR_STATUS       status;

	assert(index >= 0);
	assert(index < sv_maxclients->value);
	assert(username != NULL);
	assert(password != NULL);
	assert(s_ranked_players);
	assert(s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE);

	Com_DPrintf("SV_RankUserLogin( %d, %s, \"****\" );\n", index, username);

	if(!s_rankings_active)
	{
		Com_DPrintf("SV_RankUserLogin: Not ready for login\n");
		s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
		return;
	}

	if(s_ranked_players[index].grank_status == QGR_STATUS_ACTIVE)
	{
		Com_DPrintf("SV_RankUserLogin: Got Login from active player\n");
		return;
	}

	// get a separate context for the new user
	init = GRankInit(0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END);
	s_ranked_players[index].context = init.context;
	s_rankings_contexts++;
	Com_DPrintf("SV_RankUserLogin(); s_rankings_contexts=%d\n", s_rankings_contexts);
	Com_DPrintf("SV_RankUserLogin(); s_ranked_players[%d].context=%d\n", index, init.context);

	// login user, proceed to SV_RankUserCBF
	status = GRankUserLoginAsync
		(s_ranked_players[index].context, username, password, SV_RankUserCBF, (void *)&s_ranked_players[index], GR_OPT_END);

	if(status == GR_STATUS_PENDING)
	{
		s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
		s_ranked_players[index].final_status = QGR_STATUS_NEW;
	}
	else
	{
		SV_RankError("SV_RankUserLogin: Expected GR_STATUS_PENDING, got %s", SV_RankStatusString(status));
	}
}

/*
===================
SV_RankUserValidate
===================
*/
qboolean SV_RankUserValidate(int index, const char *player_id, const char *key, int token_len, int rank, char *name)
{
	GR_INIT         init;
	GR_STATUS       status;
	qboolean        rVal;
	ranked_player_t *ranked_player;
	int             i;

	assert(s_ranked_players);
	assert(s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE);

	rVal = qfalse;

	if(!s_rankings_active)
	{
		Com_DPrintf("SV_RankUserValidate: Not ready to validate\n");
		s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
		return rVal;
	}

	ranked_player = &(s_ranked_players[index]);

	if((player_id != NULL) && (key != NULL))
	{
		// the real player_id and key is set when SV_RankJoinGameCBF
		// is called we do this so that SV_RankUserValidate
		// can be shared by both server side login and client side login

		// for client side logined in players
		// server is creating GR_OPT_PLAYERCONTEXT
		init = GRankInit(0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END);
		ranked_player->context = init.context;
		s_rankings_contexts++;
		Com_DPrintf("SV_RankUserValidate(); s_rankings_contexts=%d\n", s_rankings_contexts);
		Com_DPrintf("SV_RankUserValidate(); s_ranked_players[%d].context=%d\n", index, init.context);

		// uudecode player id and player token
		ranked_player->player_id = SV_RankDecodePlayerID(player_id);
		Com_DPrintf("SV_RankUserValidate(); ranked_player->player_id =%u\n", (uint32_t) ranked_player->player_id);
		SV_RankDecodePlayerKey(key, ranked_player->token);

		// save name and check for duplicates
		Q_strncpyz(ranked_player->name, name, sizeof(ranked_player->name));
		for(i = 0; i < sv_maxclients->value; i++)
		{
			if((i != index) && (s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE) &&
			   (strcmp(s_ranked_players[i].name, name) == 0))
			{
				Com_DPrintf("SV_RankUserValidate: Duplicate login\n");
				ranked_player->grank_status = QGR_STATUS_NO_USER;
				ranked_player->final_status = QGR_STATUS_NEW;
				ranked_player->grank = 0;
				return qfalse;
			}
		}

		// then validate
		status = GRankPlayerValidate(s_server_context,
									 ranked_player->player_id,
									 ranked_player->token, token_len, GR_OPT_PLAYERCONTEXT, ranked_player->context, GR_OPT_END);
	}
	else
	{
		// make server side login (bots) happy
		status = GR_STATUS_OK;
	}

	if(status == GR_STATUS_OK)
	{
		ranked_player->grank_status = QGR_STATUS_ACTIVE;
		ranked_player->final_status = QGR_STATUS_NEW;
		ranked_player->grank = rank;
		rVal = qtrue;
	}
	else if(status == GR_STATUS_INVALIDUSER)
	{
		ranked_player->grank_status = QGR_STATUS_INVALIDUSER;
		ranked_player->final_status = QGR_STATUS_NEW;
		ranked_player->grank = 0;
		rVal = qfalse;
	}
	else
	{
		SV_RankError("SV_RankUserValidate: Unexpected status %s", SV_RankStatusString(status));
		s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
		ranked_player->grank = 0;
	}

	return rVal;
}

/*
================
SV_RankUserLogout
================
*/
void SV_RankUserLogout(int index)
{
	GR_STATUS       status;
	GR_STATUS       cleanup_status;

	if(!s_rankings_active)
	{
		return;
	}

	assert(index >= 0);
	assert(index < sv_maxclients->value);
	assert(s_ranked_players);

	if(s_ranked_players[index].context == 0)
	{
		return;
	}

	Com_DPrintf("SV_RankUserLogout( %d );\n", index);

	// masqueraded player may not be active yet, if they fail validation, 
	// but still they have a context needs to be cleaned 
	// what matters is the s_ranked_players[index].context

	// send reports, proceed to SV_RankSendReportsCBF
	status = GRankSendReportsAsync
		(s_ranked_players[index].context, 0, SV_RankSendReportsCBF, (void *)&s_ranked_players[index], GR_OPT_END);

	if(status == GR_STATUS_PENDING)
	{
		s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
		s_ranked_players[index].final_status = QGR_STATUS_NEW;
	}
	else
	{
		SV_RankError("SV_RankUserLogout: Expected GR_STATUS_PENDING, got %s", SV_RankStatusString(status));

		cleanup_status = GRankCleanupAsync
			(s_ranked_players[index].context, 0, SV_RankCleanupCBF, (void *)&s_ranked_players[index], GR_OPT_END);

		if(cleanup_status != GR_STATUS_PENDING)
		{
			SV_RankError("SV_RankUserLogout: Expected "
						 "GR_STATUS_PENDING from GRankCleanupAsync, got %s", SV_RankStatusString(cleanup_status));
			SV_RankCloseContext(&(s_ranked_players[index]));
		}
	}
}

/*
================
SV_RankReportInt
================
*/
void SV_RankReportInt(int index1, int index2, int key, int value, qboolean accum)
{
	GR_STATUS       status;
	GR_CONTEXT      context;
	uint64_t        match;
	uint64_t        user1;
	uint64_t        user2;
	int             opt_accum;

	if(!s_rankings_active)
	{
		return;
	}

	assert(index1 >= -1);
	assert(index1 < sv_maxclients->value);
	assert(index2 >= -1);
	assert(index2 < sv_maxclients->value);
	assert(s_ranked_players);

//  Com_DPrintf( "SV_RankReportInt( %d, %d, %d, %d, %d );\n", index1, index2, 
//      key, value, accum );

	// get context, match, and player_id for player index1
	if(index1 == -1)
	{
		context = s_server_context;
		match = s_server_match;
		user1 = 0;
	}
	else
	{
		if(s_ranked_players[index1].grank_status != QGR_STATUS_ACTIVE)
		{
			Com_DPrintf("SV_RankReportInt: Expecting QGR_STATUS_ACTIVE"
						" Got Unexpected status %d for player %d\n", s_ranked_players[index1].grank_status, index1);
			return;
		}

		context = s_ranked_players[index1].context;
		match = s_ranked_players[index1].match;
		user1 = s_ranked_players[index1].player_id;
	}

	// get player_id for player index2
	if(index2 == -1)
	{
		user2 = 0;
	}
	else
	{
		if(s_ranked_players[index2].grank_status != QGR_STATUS_ACTIVE)
		{
			Com_DPrintf("SV_RankReportInt: Expecting QGR_STATUS_ACTIVE"
						" Got Unexpected status %d for player %d\n", s_ranked_players[index2].grank_status, index2);
			return;
		}

		user2 = s_ranked_players[index2].player_id;
	}

	opt_accum = accum ? GR_OPT_ACCUM : GR_OPT_END;

	status = GRankReportInt(context, match, user1, user2, key, value, opt_accum, GR_OPT_END);

	if(status != GR_STATUS_OK)
	{
		SV_RankError("SV_RankReportInt: Unexpected status %s", SV_RankStatusString(status));
	}

	if(user2 != 0)
	{
		context = s_ranked_players[index2].context;
		match = s_ranked_players[index2].match;

		status = GRankReportInt(context, match, user1, user2, key, value, opt_accum, GR_OPT_END);

		if(status != GR_STATUS_OK)
		{
			SV_RankError("SV_RankReportInt: Unexpected status %s", SV_RankStatusString(status));
		}
	}
}

/*
================
SV_RankReportStr
================
*/
void SV_RankReportStr(int index1, int index2, int key, char *value)
{
	GR_STATUS       status;
	GR_CONTEXT      context;
	uint64_t        match;
	uint64_t        user1;
	uint64_t        user2;

	if(!s_rankings_active)
	{
		return;
	}

	assert(index1 >= -1);
	assert(index1 < sv_maxclients->value);
	assert(index2 >= -1);
	assert(index2 < sv_maxclients->value);
	assert(s_ranked_players);

//  Com_DPrintf( "SV_RankReportStr( %d, %d, %d, \"%s\" );\n", index1, index2, 
//      key, value );

	// get context, match, and player_id for player index1
	if(index1 == -1)
	{
		context = s_server_context;
		match = s_server_match;
		user1 = 0;
	}
	else
	{
		if(s_ranked_players[index1].grank_status != QGR_STATUS_ACTIVE)
		{
			Com_DPrintf("SV_RankReportStr: Unexpected status %d\n", s_ranked_players[index1].grank_status);
			return;
		}

		context = s_ranked_players[index1].context;
		match = s_ranked_players[index1].match;
		user1 = s_ranked_players[index1].player_id;
	}

	// get player_id for player index2
	if(index2 == -1)
	{
		user2 = 0;
	}
	else
	{
		if(s_ranked_players[index2].grank_status != QGR_STATUS_ACTIVE)
		{
			Com_DPrintf("SV_RankReportStr: Unexpected status %d\n", s_ranked_players[index2].grank_status);
			return;
		}

		user2 = s_ranked_players[index2].player_id;
	}

	status = GRankReportStr(context, match, user1, user2, key, value, GR_OPT_END);

	if(status != GR_STATUS_OK)
	{
		SV_RankError("SV_RankReportStr: Unexpected status %s", SV_RankStatusString(status));
	}

	if(user2 != 0)
	{
		context = s_ranked_players[index2].context;
		match = s_ranked_players[index2].match;

		status = GRankReportStr(context, match, user1, user2, key, value, GR_OPT_END);

		if(status != GR_STATUS_OK)
		{
			SV_RankError("SV_RankReportInt: Unexpected status %s", SV_RankStatusString(status));
		}
	}
}

/*
================
SV_RankQuit
================
*/
void SV_RankQuit(void)
{
	int             i;
	int             j = 0;

	// yuck

	while(s_rankings_contexts > 1)
	{
		assert(s_ranked_players);
		if(s_ranked_players != NULL)
		{
			for(i = 0; i < sv_maxclients->value; i++)
			{
				// check for players that weren't yet active in SV_RankEnd
				if(s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE)
				{
					SV_RankUserLogout(i);
					Com_DPrintf("SV_RankQuit: SV_RankUserLogout %d\n", i);
				}
				else
				{
					if(s_ranked_players[i].context)
					{
						GR_STATUS       cleanup_status;

						cleanup_status = GRankCleanupAsync
							(s_ranked_players[i].context, 0, SV_RankCleanupCBF, (void *)&(s_ranked_players[i]), GR_OPT_END);

						if(cleanup_status != GR_STATUS_PENDING)
						{
							SV_RankError("SV_RankQuit: Expected "
										 "GR_STATUS_PENDING from GRankCleanupAsync, got %s", SV_RankStatusString(cleanup_status));
						}
					}
				}
			}
		}
		SV_RankPoll();

		// should've finished by now
		assert((j++) < 68);
	}
}

/*
==============================================================================

Private Functions

==============================================================================
*/

/*
=================
SV_RankNewGameCBF
=================
*/
static void SV_RankNewGameCBF(GR_NEWGAME * gr_newgame, void *cbf_arg)
{
	GR_MATCH        match;
	int             i;

	assert(gr_newgame != NULL);
	assert(cbf_arg == NULL);

	Com_DPrintf("SV_RankNewGameCBF( %08X, %08X );\n", gr_newgame, cbf_arg);

	if(gr_newgame->status == GR_STATUS_OK)
	{
		char            info[MAX_INFO_STRING];
		char            gameid[sizeof(s_ranked_players[i].game_id) * 4 / 3 + 2];

		// save game id
		s_rankings_game_id = gr_newgame->game_id;

		// encode gameid 
		memset(gameid, 0, sizeof(gameid));
		SV_RankEncodeGameID(s_rankings_game_id, gameid, sizeof(gameid));

		// set CS_GRANK rankingsGameID to pass to client
		memset(info, 0, sizeof(info));
		Info_SetValueForKey(info, "rankingsGameKey", s_rankings_game_key);
		Info_SetValueForKey(info, "rankingsGameID", gameid);
		SV_SetConfigstring(CS_GRANK, info);

		// initialize client status
		for(i = 0; i < sv_maxclients->value; i++)
			s_ranked_players[i].grank_status = QGR_STATUS_NEW;

		// start new match
		match = GRankStartMatch(s_server_context);
		s_server_match = match.match;

		// ready to go
		s_rankings_active = qtrue;
		Cvar_Set("sv_rankingsActive", "1");

	}
	else if(gr_newgame->status == GR_STATUS_BADLEAGUE)
	{
		SV_RankError("SV_RankNewGameCBF: Invalid League name\n");
	}
	else
	{
		//GRank handle new game failure
		// force  SV_RankEnd() to run
		//SV_RankEnd();
		SV_RankError("SV_RankNewGameCBF: Unexpected status %s", SV_RankStatusString(gr_newgame->status));
	}
}

/*
================
SV_RankUserCBF
================
*/
static void SV_RankUserCBF(GR_LOGIN * gr_login, void *cbf_arg)
{
	ranked_player_t *ranked_player;
	GR_STATUS       join_status;
	GR_STATUS       cleanup_status;

	assert(gr_login != NULL);
	assert(cbf_arg != NULL);

	Com_DPrintf("SV_RankUserCBF( %08X, %08X );\n", gr_login, cbf_arg);

	ranked_player = (ranked_player_t *) cbf_arg;
	assert(ranked_player);
	assert(ranked_player->context);

	switch (gr_login->status)
	{
		case GR_STATUS_OK:
			// attempt to join the game, proceed to SV_RankJoinGameCBF
			join_status = GRankJoinGameAsync(ranked_player->context, s_rankings_game_id, SV_RankJoinGameCBF, cbf_arg, GR_OPT_END);

			if(join_status != GR_STATUS_PENDING)
			{
				SV_RankError("SV_RankUserCBF: Expected GR_STATUS_PENDING "
							 "from GRankJoinGameAsync, got %s", SV_RankStatusString(join_status));
			}
			break;
		case GR_STATUS_NOUSER:
			Com_DPrintf("SV_RankUserCBF: Got status %s\n", SV_RankStatusString(gr_login->status));
			ranked_player->final_status = QGR_STATUS_NO_USER;
			break;
		case GR_STATUS_BADPASSWORD:
			Com_DPrintf("SV_RankUserCBF: Got status %s\n", SV_RankStatusString(gr_login->status));
			ranked_player->final_status = QGR_STATUS_BAD_PASSWORD;
			break;
		case GR_STATUS_TIMEOUT:
			Com_DPrintf("SV_RankUserCBF: Got status %s\n", SV_RankStatusString(gr_login->status));
			ranked_player->final_status = QGR_STATUS_TIMEOUT;
			break;
		default:
			Com_DPrintf("SV_RankUserCBF: Unexpected status %s\n", SV_RankStatusString(gr_login->status));
			ranked_player->final_status = QGR_STATUS_ERROR;
			break;
	}

	if(ranked_player->final_status != QGR_STATUS_NEW)
	{
		// login or create failed, so clean up before the next attempt
		cleanup_status = GRankCleanupAsync(ranked_player->context, 0, SV_RankCleanupCBF, (void *)ranked_player, GR_OPT_END);

		if(cleanup_status != GR_STATUS_PENDING)
		{
			SV_RankError("SV_RankUserCBF: Expected GR_STATUS_PENDING "
						 "from GRankCleanupAsync, got %s", SV_RankStatusString(cleanup_status));
			SV_RankCloseContext(ranked_player);
		}
	}
}

/*
================
SV_RankJoinGameCBF
================
*/
static void SV_RankJoinGameCBF(GR_JOINGAME * gr_joingame, void *cbf_arg)
{
	ranked_player_t *ranked_player;
	GR_MATCH        match;
	GR_STATUS       cleanup_status;

	assert(gr_joingame != NULL);
	assert(cbf_arg != NULL);

	Com_DPrintf("SV_RankJoinGameCBF( %08X, %08X );\n", gr_joingame, cbf_arg);

	ranked_player = (ranked_player_t *) cbf_arg;

	assert(ranked_player);
	assert(ranked_player->context != 0);

	if(gr_joingame->status == GR_STATUS_OK)
	{
		int             i;

		// save user id
		ranked_player->player_id = gr_joingame->player_id;
		memcpy(ranked_player->token, gr_joingame->token, sizeof(GR_PLAYER_TOKEN));
		match = GRankStartMatch(ranked_player->context);
		ranked_player->match = match.match;
		ranked_player->grank = gr_joingame->rank;

		// find the index and call SV_RankUserValidate
		for(i = 0; i < sv_maxclients->value; i++)
			if(ranked_player == &s_ranked_players[i])
				SV_RankUserValidate(i, NULL, NULL, 0, gr_joingame->rank, ranked_player->name);
	}
	else
	{
		//GRand handle join game failure
		SV_RankError("SV_RankJoinGameCBF: Unexpected status %s", SV_RankStatusString(gr_joingame->status));

		cleanup_status = GRankCleanupAsync(ranked_player->context, 0, SV_RankCleanupCBF, cbf_arg, GR_OPT_END);

		if(cleanup_status != GR_STATUS_PENDING)
		{
			SV_RankError("SV_RankJoinGameCBF: Expected "
						 "GR_STATUS_PENDING from GRankCleanupAsync, got %s", SV_RankStatusString(cleanup_status));
			SV_RankCloseContext(ranked_player);
		}
	}
}

/*
================
SV_RankSendReportsCBF
================
*/
static void SV_RankSendReportsCBF(GR_STATUS * status, void *cbf_arg)
{
	ranked_player_t *ranked_player;
	GR_CONTEXT      context;
	GR_STATUS       cleanup_status;

	assert(status != NULL);
	// NULL cbf_arg means server is sending match reports

	Com_DPrintf("SV_RankSendReportsCBF( %08X, %08X );\n", status, cbf_arg);

	ranked_player = (ranked_player_t *) cbf_arg;
	if(ranked_player == NULL)
	{
		Com_DPrintf("SV_RankSendReportsCBF: server\n");
		context = s_server_context;
	}
	else
	{
		Com_DPrintf("SV_RankSendReportsCBF: player\n");
		context = ranked_player->context;
	}

	//assert( context != 0 );
	if(*status != GR_STATUS_OK)
	{
		SV_RankError("SV_RankSendReportsCBF: Unexpected status %s", SV_RankStatusString(*status));
	}

	if(context == 0)
	{
		Com_DPrintf("SV_RankSendReportsCBF: WARNING: context == 0");
		SV_RankCloseContext(ranked_player);
	}
	else
	{
		cleanup_status = GRankCleanupAsync(context, 0, SV_RankCleanupCBF, cbf_arg, GR_OPT_END);

		if(cleanup_status != GR_STATUS_PENDING)
		{
			SV_RankError("SV_RankSendReportsCBF: Expected "
						 "GR_STATUS_PENDING from GRankCleanupAsync, got %s", SV_RankStatusString(cleanup_status));
			SV_RankCloseContext(ranked_player);
		}
	}
}

/*
================
SV_RankCleanupCBF
================
*/
static void SV_RankCleanupCBF(GR_STATUS * status, void *cbf_arg)
{
	ranked_player_t *ranked_player;

	ranked_player = (ranked_player_t *) cbf_arg;

	assert(status != NULL);
	// NULL cbf_arg means server is cleaning up

	Com_DPrintf("SV_RankCleanupCBF( %08X, %08X );\n", status, cbf_arg);

	if(*status != GR_STATUS_OK)
	{
		SV_RankError("SV_RankCleanupCBF: Unexpected status %s", SV_RankStatusString(*status));
	}

	SV_RankCloseContext(ranked_player);
}

/*
================
SV_RankCloseContext
================
*/
static void SV_RankCloseContext(ranked_player_t * ranked_player)
{
	if(ranked_player == NULL)
	{
		// server cleanup
		if(s_server_context == 0)
		{
			return;
		}
		s_server_context = 0;
		s_server_match = 0;
	}
	else
	{
		// player cleanup
		if(s_ranked_players == NULL)
		{
			return;
		}
		if(ranked_player->context == 0)
		{
			return;
		}
		ranked_player->context = 0;
		ranked_player->match = 0;
		ranked_player->player_id = 0;
		memset(ranked_player->token, 0, sizeof(GR_PLAYER_TOKEN));
		ranked_player->grank_status = ranked_player->final_status;
		ranked_player->final_status = QGR_STATUS_NEW;
		ranked_player->name[0] = '\0';
	}

	assert(s_rankings_contexts > 0);
	s_rankings_contexts--;
	Com_DPrintf("SV_RankCloseContext: s_rankings_contexts = %d\n", s_rankings_contexts);

	if(s_rankings_contexts == 0)
	{
		GRankLogLevel(GRLOG_OFF);

		if(s_ranked_players != NULL)
		{
			Z_Free(s_ranked_players);
			s_ranked_players = NULL;
		}

		s_rankings_active = qfalse;
		Cvar_Set("sv_rankingsActive", "0");
	}
}

/*
================
SV_RankAsciiEncode

Encodes src_len bytes of binary data from the src buffer as ASCII text, 
using 6 bits per character. The result string is null-terminated and 
stored in the dest buffer.

The dest buffer must be at least (src_len * 4) / 3 + 2 bytes in length.

Returns the length of the result string, not including the null.
================
*/
static int SV_RankAsciiEncode(char *dest, const unsigned char *src, int src_len)
{
	unsigned char   bin[3];
	unsigned char   txt[4];
	int             dest_len = 0;
	int             i;
	int             j;
	int             num_chars;

	assert(dest != NULL);
	assert(src != NULL);

	for(i = 0; i < src_len; i += 3)
	{
		// read three bytes of input
		for(j = 0; j < 3; j++)
		{
			bin[j] = (i + j < src_len) ? src[i + j] : 0;
		}

		// get four 6-bit values from three bytes
		txt[0] = bin[0] >> 2;
		txt[1] = ((bin[0] << 4) | (bin[1] >> 4)) & 63;
		txt[2] = ((bin[1] << 2) | (bin[2] >> 6)) & 63;
		txt[3] = bin[2] & 63;

		// store ASCII encoding of 6-bit values
		num_chars = (i + 2 < src_len) ? 4 : ((src_len - i) * 4) / 3 + 1;
		for(j = 0; j < num_chars; j++)
		{
			dest[dest_len++] = s_ascii_encoding[txt[j]];
		}
	}

	dest[dest_len] = '\0';

	return dest_len;
}

/*
================
SV_RankAsciiDecode

Decodes src_len characters of ASCII text from the src buffer, stores 
the binary result in the dest buffer.

The dest buffer must be at least (src_len * 3) / 4 bytes in length.

Returns the length of the binary result, or zero for invalid input.
================
*/
static int SV_RankAsciiDecode(unsigned char *dest, const char *src, int src_len)
{
	static unsigned char s_inverse_encoding[256];
	static char     s_init = 0;

	unsigned char   bin[3];
	unsigned char   txt[4];
	int             dest_len = 0;
	int             i;
	int             j;
	int             num_bytes;

	assert(dest != NULL);
	assert(src != NULL);

	if(!s_init)
	{
		// initialize lookup table for decoding
		memset(s_inverse_encoding, 255, sizeof(s_inverse_encoding));
		for(i = 0; i < 64; i++)
		{
			s_inverse_encoding[s_ascii_encoding[i]] = i;
		}
		s_init = 1;
	}

	for(i = 0; i < src_len; i += 4)
	{
		// read four characters of input, decode them to 6-bit values
		for(j = 0; j < 4; j++)
		{
			txt[j] = (i + j < src_len) ? s_inverse_encoding[src[i + j]] : 0;
			if(txt[j] == 255)
			{
				return 0;		// invalid input character
			}
		}

		// get three bytes from four 6-bit values
		bin[0] = (txt[0] << 2) | (txt[1] >> 4);
		bin[1] = (txt[1] << 4) | (txt[2] >> 2);
		bin[2] = (txt[2] << 6) | txt[3];

		// store binary data
		num_bytes = (i + 3 < src_len) ? 3 : ((src_len - i) * 3) / 4;
		for(j = 0; j < num_bytes; j++)
		{
			dest[dest_len++] = bin[j];
		}
	}

	return dest_len;
}

/*
================
SV_RankEncodeGameID
================
*/
static void SV_RankEncodeGameID(uint64_t game_id, char *result, int len)
{
	assert(result != NULL);

	if(len < ((sizeof(game_id) * 4) / 3 + 2))
	{
		Com_DPrintf("SV_RankEncodeGameID: result buffer too small\n");
		result[0] = '\0';
	}
	else
	{
		qint64          gameid = LittleLong64(*(qint64 *) & game_id);

		SV_RankAsciiEncode(result, (unsigned char *)&gameid, sizeof(qint64));
	}
}

/*
================
SV_RankDecodePlayerID
================
*/
static uint64_t SV_RankDecodePlayerID(const char *string)
{
	unsigned char   buffer[9];
	int             len;
	qint64          player_id;

	assert(string != NULL);

	len = strlen(string);
	Com_DPrintf("SV_RankDecodePlayerID: string length %d\n", len);
	SV_RankAsciiDecode(buffer, string, len);
	player_id = LittleLong64(*(qint64 *) buffer);
	return *(uint64_t *) & player_id;
}

/*
================
SV_RankDecodePlayerKey
================
*/
static void SV_RankDecodePlayerKey(const char *string, GR_PLAYER_TOKEN key)
{
	unsigned char   buffer[1400];
	int             len;

	assert(string != NULL);

	len = strlen(string);
	Com_DPrintf("SV_RankDecodePlayerKey: string length %d\n", len);

	memset(key, 0, sizeof(GR_PLAYER_TOKEN));
	memset(buffer, 0, sizeof(buffer));
	memcpy(key, buffer, SV_RankAsciiDecode(buffer, string, len));
}

/*
================
SV_RankStatusString
================
*/
static char    *SV_RankStatusString(GR_STATUS status)
{
	switch (status)
	{
		case GR_STATUS_OK:
			return "GR_STATUS_OK";
		case GR_STATUS_ERROR:
			return "GR_STATUS_ERROR";
		case GR_STATUS_BADPARAMS:
			return "GR_STATUS_BADPARAMS";
		case GR_STATUS_NETWORK:
			return "GR_STATUS_NETWORK";
		case GR_STATUS_NOUSER:
			return "GR_STATUS_NOUSER";
		case GR_STATUS_BADPASSWORD:
			return "GR_STATUS_BADPASSWORD";
		case GR_STATUS_BADGAME:
			return "GR_STATUS_BADGAME";
		case GR_STATUS_PENDING:
			return "GR_STATUS_PENDING";
		case GR_STATUS_BADDOMAIN:
			return "GR_STATUS_BADDOMAIN";
		case GR_STATUS_DOMAINLOCK:
			return "GR_STATUS_DOMAINLOCK";
		case GR_STATUS_TIMEOUT:
			return "GR_STATUS_TIMEOUT";
		case GR_STATUS_INVALIDUSER:
			return "GR_STATUS_INVALIDUSER";
		case GR_STATUS_INVALIDCONTEXT:
			return "GR_STATUS_INVALIDCONTEXT";
		default:
			return "(UNKNOWN)";
	}
}

/*
================
SV_RankError
================
*/
static void SV_RankError(const char *fmt, ...)
{
	va_list         arg_ptr;
	char            text[1024];

	va_start(arg_ptr, fmt);
	Q_vsnprintf(text, sizeof(text), fmt, arg_ptr);
	va_end(arg_ptr);

	Com_DPrintf("****************************************\n");
	Com_DPrintf("SV_RankError: %s\n", text);
	Com_DPrintf("****************************************\n");

	s_rankings_active = qfalse;
	Cvar_Set("sv_rankingsActive", "0");
	// FIXME - attempt clean shutdown?
}
